進行到這邊之後,目前依照我的電腦情況,來記錄一下目前最耗時的銳利化濾鏡平均需要耗費的處理時間 ( 單一個濾鏡效果做測試 )
濾鏡 | 時間( 毫秒 100次平均 ) |
---|---|
銳利化 | 65 |
而因為這個的確是需要大量計算,所以看起來很適合交給 Webassembly
來計算,那接下來就開始吧!
前面有介紹到在 Rust
裡面使用時,我們有使用 #[wasm_bindgen]
這種用法
#[wasm_bindgen]
pub fn fib(i: u32) -> u32 {
match i {
0 => 0,
1 => 1,
_ => fib(i-1) + fib(i-2)
}
}
這個主要是告訴打包過程中,這個 function
將會要匯出被使用,所以可以看出最終打包的成果
index_bg.wasm
會是之前說的二進制的程式,而 index.js
則是定義了有哪些 function
可以使用
/**
* @param {number} i
* @returns {number}
*/
export function fib(i) {
const ret = wasm.fib(i);
return ret >>> 0;
}
所以如果沒有加上 #[wasm_bindgen]
的話,是不會出現在這裡的。而除了幫我們匯出之外,因為目前原生的 Webassembly
只支援了 int32
、int64
、float32
和 float64
,這幾種型式,也就是說如果我們想跟 JS
使用不同型態值的話會遇到很大的麻煩,因此透過 #[wasm_bindgen]
會幫我們產生可以跟 JS
的程式碼,讓我們專注在邏輯上。
詳細
Webassembly
與瀏覽器交互可參考這篇文章
另外我們還會需要使用 web_sys 來讓我們可以傳入原生的 Html Element
以及使用 js_sys 來使用 JS
的原生型別,所以加入在 cargo.toml
加入
[dependencies.web-sys]
version = "0.3.4"
features = [
'ImageData',
'console'
]
[dependencies.js-sys]
version = "^0.3"
這樣我們就可以來實際寫我們的 Rust
程式碼了,首先宣告一些我們該使用的東西
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::{ ImageData, console };
use js_sys::Math:: { sqrt, floor };
接著把原本用 Rust
實作一次,這邊卡了蠻久的,主要是因為需要定義好各種型別,為了想跟 JS
的程式碼盡可能相似,所以會發現我在下面一直有做強制轉型的動作,不然應該會有更好的寫法。另外要注意的是要記的超出範圍的問題,因為 Rust
在轉成 u8
的形式時,超出範圍的不會回歸到該範圍最大值,如果是 260 轉成 255 時,結果會變成 5 ,所以下面要自己實作 clamp
的操作。
#[wasm_bindgen]
pub fn convolve(val: ImageData, kernel: &[i16], amount: f32) -> std::result::Result<web_sys::ImageData, wasm_bindgen::JsValue>{
let imageHeight: u32 = val.height();
let imageWidth: u32 = val.width();
let pixelData = val.data();
let side: u32 = sqrt(kernel.len() as f64) as u32;
let half: u32 = floor((side / 2).into()) as u32;
let mut outputPixelData = val.data().clone();
for y in 0..imageHeight {
for x in 0..imageWidth {
let dstOff = (y * imageWidth + x) * 4;
let mut totalR = 0;
let mut totalG = 0;
let mut totalB = 0;
for row in 0..side {
for col in 0..side {
let srcY = y + row - half;
let srcX = x + col - half;
if srcY < 0 || srcY >= imageHeight || srcX < 0 || srcX >= imageWidth {
continue
}
let srcOff = (srcY * imageWidth + srcX) * 4;
let weight = kernel[(row * side + col) as usize];
let [r, g, b] = [
pixelData[srcOff as usize],
pixelData[(srcOff + 1) as usize],
pixelData[(srcOff + 2) as usize]
];
totalR += r as i16 * weight;
totalG += g as i16 * weight;
totalB += b as i16 * weight;
}
}
if amount > 0.0 {
outputPixelData[dstOff as usize] =
clamp(totalR as f32 * amount + outputPixelData[dstOff as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
outputPixelData[(dstOff + 1) as usize] =
clamp(totalG as f32 * amount + outputPixelData[(dstOff + 1) as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
outputPixelData[(dstOff + 2) as usize] =
clamp(totalB as f32 * amount + outputPixelData[(dstOff + 2) as usize] as f32 * (1.0 - amount), 0.0, 255.0 );
}
}
}
ImageData::new_with_u8_clamped_array_and_sh(Clamped(&mut outputPixelData), imageWidth, imageHeight)
}
fn clamp(input: f32, min: f32, max: f32) -> u8 {
if input > max {
max as u8
}
else if input < min {
min as u8
} else {
input as u8
}
}
最後沒問題的話一樣在原本的 function
裡面使用,測試結果如下
濾鏡 | 時間( 毫秒 100次平均 ) |
---|---|
銳利化 | 42 |
目前結果也大約是快了將近 30%,雖然還是沒辦法達成 60 fps,的目標,但也是縮短了許多計算時間。但值得注意的是在把其他計算量比較低的濾鏡如曝光度改成使用 Webassembly
,實際上耗費的時間甚至會比原本還慢一點點,因為在溝通上也是會需要耗上一些額外的成本,所以在使用上也不是所有的計算都交給 Webassembly
,要評估一下。
目前在引用到
Worker
的時候Webpack
一直會出錯,所以這邊就沒做在畫面上,如果有興趣使用請自行將程式碼手動開啟
列城的俯瞰照片,在馬路上其實都是塵土飛揚,車子經過都是滿滿的風沙
優化的部分到這邊總算結束了,而當初設定的兩個目標
Worker
,並且直接使用 OffscreenCanvas
及 ImageBitmap
在 Worker
中渲染,大約省了 20% 的時間,並且維持主線程 UI
不卡頓Rust
實作 Webassembly
,在特定濾鏡上大約省了 30% 的時間雖然期待最終完整版本應該是用 Webassembly
在 Worker
中直接渲染,但是因為一直找不到設定哪裡出錯,所以就先跳過了。也希望在 Webpack 5
發布後,未來這些用法也會更佳的直覺,不會現像在一樣需要一堆設定。接下來應該會介紹一些其他影像相關的 library
,明天見!